פיתחו את הפוטנציאל המלא של פנדס על ידי שליטה בפונקציות מותאמות אישית. מדריך מקיף זה מפרט את ההבדלים, הביצועים ומקרי השימוש הטובים ביותר עבור apply(), map(), ו-applymap().
שליטה בפנדס: צלילה עמוקה לפונקציות מותאמות אישית עם apply(), map(), ו-applymap()
בעולם של מדעי הנתונים והניתוח, ספריית Pandas של פייתון היא כלי הכרחי. היא מספקת מבני נתונים חזקים, גמישים ויעילים המיועדים להפוך את העבודה עם נתונים מובנים לקלה ואינטואיטיבית. בעוד ש-Pandas מגיעה עם סט עשיר של פונקציות מובנות לאיגום, סינון והמרה, מגיע שלב במסעו של כל איש מקצוע בתחום הנתונים שבו אלה אינם מספיקים. עליך ליישם את הלוגיקה המותאמת אישית שלך, כלל עסקי ייחודי או המרה מורכבת שאינה זמינה בקלות.
זה המקום שבו היכולת ליישם פונקציות מותאמות אישית הופכת לכוח-על. עם זאת, Pandas מציעה מספר דרכים להשגת זאת, בעיקר באמצעות השיטות apply(), map() ו-applymap(). למצטרף החדש, פונקציות אלה יכולות להיראות דומות באופן מבלבל. באיזו מהן כדאי להשתמש? מתי? ומהן ההשלכות על הביצועים של הבחירה שלך?
מדריך מקיף זה יסיר את המסתורין משיטות עוצמתיות אלו. אנו נחקור כל אחת מהן בפירוט, נבין את מקרי השימוש הספציפיים שלהן, והכי חשוב, נלמד כיצד לבחור את הכלי הנכון עבור העבודה כדי לכתוב קוד Pandas נקי, יעיל וקריא. אנו נכסה את הנושאים הבאים:
- שיטת
map(): אידיאלית להמרה אלמנט-אחר-אלמנט על Series בודדת. - שיטת
apply(): סוס העבודה הרב-תכליתי לפעולות לפי שורה או לפי עמודה על DataFrame. - שיטת
applymap(): המומחה לפעולות אלמנט-אחר-אלמנט על פני DataFrame שלם. - שיקולי ביצועים: ההבדל הקריטי בין שיטות אלה לוקטוריזציה אמיתית.
- שיטות עבודה מומלצות: מסגרת קבלת החלטות שתעזור לך לבחור את השיטה היעילה ביותר בכל פעם.
הצבת התפאורה: מערך הנתונים לדוגמה שלנו
כדי להפוך את הדוגמאות שלנו למעשיות וברורות, בואו נעבוד עם מערך נתונים עקבי ורלוונטי גלובלית. ניצור DataFrame לדוגמה המייצג נתוני מכירות מקוונות מחברת מסחר אלקטרוני בינלאומית בדיונית.
import pandas as pd
import numpy as np
data = {
'OrderID': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
'Product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Webcam', 'Headphones', 'Docking Station', 'Mouse'],
'Category': ['Electronics', 'Accessories', 'Accessories', 'Electronics', 'Accessories', 'Audio', 'Electronics', 'Accessories'],
'Price_USD': [1200, 25, 75, 300, 50, 150, 250, 30],
'Quantity': [1, 2, 1, 2, 1, 1, 1, 3],
'Country': ['USA', 'Canada', 'USA', 'Germany', 'Japan', 'Canada', 'Germany', np.nan]
}
df = pd.DataFrame(data)
print(df)
DataFrame זה נותן לנו תערובת נחמדה של סוגי נתונים (מספריים, מחרוזות, ואפילו ערך חסר) כדי להדגים את היכולות המלאות של הפונקציות המכוונות שלנו.
שיטת map(): המרה אלמנט-אחר-אלמנט עבור Series
מה זה map()?
שיטת map() היא הכלי המיוחד שלך לשינוי ערכים בתוך עמודה בודדת (Series של Pandas). היא פועלת על בסיס אלמנט-אחר-אלמנט. תחשוב על זה כאילו אומר, "עבור כל פריט בעמודה זו, חפש אותו במילון או העבר אותו דרך הפונקציה הזו והחלף אותו בתוצאה."
היא משמשת בעיקר לשני משימות:
- החלפת ערכים בהתבסס על מילון (מיפוי).
- החלת פונקציה פשוטה על כל אלמנט.
מקרה שימוש 1: מיפוי ערכים עם מילון
זהו השימוש הנפוץ והיעיל ביותר של map(). דמיין שאנו רוצים ליצור עמודה 'מחלקה' רחבה יותר המבוססת על העמודה 'קטגוריה' שלנו. אנו יכולים להגדיר מיפוי במילון Python ולהשתמש ב-map() כדי ליישם אותו.
category_to_department = {
'Electronics': 'Technology',
'Accessories': 'Peripherals',
'Audio': 'Technology'
}
df['Department'] = df['Category'].map(category_to_department)
print(df[['Category', 'Department']])
פלט:
Category Department
0 Electronics Technology
1 Accessories Peripherals
2 Accessories Peripherals
3 Electronics Technology
4 Accessories Peripherals
5 Audio Technology
6 Electronics Technology
7 Accessories Peripherals
שימו לב עד כמה זה עובד באלגנטיות. כל ערך בסדרת 'קטגוריה' נחפש במילון `category_to_department`, והערך המתאים משמש לאכלוס העמודה 'מחלקה' החדשה. אם לא נמצא מפתח במילון, map() ייצור ערך NaN (לא מספר), שלעתים קרובות הוא ההתנהגות הרצויה עבור קטגוריות לא ממופות.
מקרה שימוש 2: החלת פונקציה עם map()
ניתן גם להעביר פונקציה (כולל פונקציית lambda) ל-map(). הפונקציה תבוצע עבור כל אלמנט בסדרה. בואו ניצור עמודה חדשה שנותנת לנו תווית תיאורית למחיר.
def price_label(price):
if price > 200:
return 'High-Value'
elif price > 50:
return 'Mid-Value'
else:
return 'Low-Value'
df['Price_Label'] = df['Price_USD'].map(price_label)
# Using a lambda function for a simpler task:
# df['Product_Length'] = df['Product'].map(lambda x: len(x))
print(df[['Product', 'Price_USD', 'Price_Label']])
פלט:
Product Price_USD Price_Label
0 Laptop 1200 High-Value
1 Mouse 25 Low-Value
2 Keyboard 75 Mid-Value
3 Monitor 300 High-Value
4 Webcam 50 Low-Value
5 Headphones 150 Mid-Value
6 Docking Station 250 High-Value
7 Mouse 30 Low-Value
מתי להשתמש ב-map(): סיכום מהיר
- אתה עובד על עמודה בודדת (Series).
- עליך להחליף ערכים בהתבסס על מילון או Series אחר. זהו כוחה העיקרי.
- עליך להחיל פונקציה פשוטה אלמנט-אחר-אלמנט על עמודה בודדת.
שיטת apply(): סוס העבודה הרב-תכליתי
מה זה apply()?
אם map() הוא מומחה, apply() הוא כוח העבודה למטרה כללית. היא גמישה יותר מכיוון שהיא יכולה לפעול על Series ו-DataFrames כאחד. המפתח להבנת apply() הוא הפרמטר axis, המכוון את פעולתו:
- על Series: היא פועלת אלמנט-אחר-אלמנט, בדומה ל-
map(). - על DataFrame עם
axis=0(ברירת המחדל): היא מחילה פונקציה על כל עמודה. הפונקציה מקבלת כל עמודה כ-Series. - על DataFrame עם
axis=1: היא מחילה פונקציה על כל שורה. הפונקציה מקבלת כל שורה כ-Series.
apply() על Series
כאשר משתמשים בה על Series, apply() מתנהגת בדומה מאוד ל-map(). היא מחילה פונקציה על כל אלמנט. לדוגמה, נוכל לשכפל את דוגמת תווית המחיר שלנו.
df['Price_Label_apply'] = df['Price_USD'].apply(price_label)
print(df['Price_Label_apply'].equals(df['Price_Label'])) # Output: True
אמנם הם נראים ניתנים להחלפה כאן, אך map() מהירה מעט יותר עבור החלפות מילון פשוטות ופעולות אלמנט-אחר-אלמנט על Series מכיוון שיש לה נתיב מותאם יותר למשימות ספציפיות אלה.
apply() על DataFrame (לפי עמודה, axis=0)
זהו המצב המוגדר כברירת מחדל עבור DataFrame. הפונקציה שאתה מספק נקראת פעם אחת עבור כל עמודה. זה שימושי עבור צבירות או המרות לפי עמודה.
בואו נמצא את ההבדל בין הערך המקסימלי למינימלי (הטווח) עבור כל העמודות המספריות שלנו.
numeric_cols = df[['Price_USD', 'Quantity']]
def get_range(column_series):
return column_series.max() - column_series.min()
column_ranges = numeric_cols.apply(get_range, axis=0)
print(column_ranges)
פלט:
Price_USD 1175.0
Quantity 2.0
dtype: float64
כאן, הפונקציה get_range קיבלה תחילה את הסדרה 'Price_USD', חישבה את הטווח שלה, ואז קיבלה את הסדרה 'Quantity' ועשתה את אותו הדבר, והחזירה סדרה חדשה עם התוצאות.
apply() על DataFrame (לפי שורה, axis=1)
זוהי, ללא ספק, מקרה השימוש החזק והנפוץ ביותר עבור apply(). כאשר עליך לחשב ערך חדש המבוסס על עמודות מרובות באותה שורה, apply() עם axis=1 הוא הפתרון שלך.
הפונקציה שאתה מעביר תקבל כל שורה כ-Series, כאשר האינדקס הוא שמות העמודות. בואו נחשב את העלות הכוללת עבור כל הזמנה.
def calculate_total_cost(row):
# 'row' is a Series representing a single row
price = row['Price_USD']
quantity = row['Quantity']
return price * quantity
df['Total_Cost'] = df.apply(calculate_total_cost, axis=1)
print(df[['Product', 'Price_USD', 'Quantity', 'Total_Cost']])
פלט:
Product Price_USD Quantity Total_Cost
0 Laptop 1200 1 1200
1 Mouse 25 2 50
2 Keyboard 75 1 75
3 Monitor 300 2 600
4 Webcam 50 1 50
5 Headphones 150 1 150
6 Docking Station 250 1 250
7 Mouse 30 3 90
זה משהו ש-map() פשוט לא יכולה לעשות, מכיוון שהיא מוגבלת לעמודה בודדת. בואו נראה דוגמה מורכבת יותר. אנו רוצים לסווג את עדיפות המשלוח של כל הזמנה בהתבסס על הקטגוריה והמדינה שלה.
def assign_shipping_priority(row):
if row['Category'] == 'Electronics' and row['Country'] == 'USA':
return 'High Priority'
elif row['Total_Cost'] > 500:
return 'High Priority'
elif row['Country'] == 'Japan':
return 'Medium Priority'
else:
return 'Standard'
df['Shipping_Priority'] = df.apply(assign_shipping_priority, axis=1)
print(df[['Category', 'Country', 'Total_Cost', 'Shipping_Priority']])
מתי להשתמש ב-apply(): סיכום מהיר
- כאשר הלוגיקה שלך תלויה במספר עמודות בשורה (השתמש ב-
axis=1). זו התכונה ההורגת שלה. - כאשר עליך להחיל פונקציית צבירה למטה עמודות או על פני שורות.
- ככלי ליישום פונקציות למטרה כללית כאשר
map()לא מתאים.
אזכור מיוחד: שיטת applymap()
מה זה applymap()?
שיטת applymap() היא מומחה נוסף, אך התחום שלה הוא ה-DataFrame כולו. היא מחילה פונקציה על כל אלמנט בודד של DataFrame. היא אינה פועלת על Series - זו שיטה של DataFrame בלבד.
תחשוב על זה כריצה של map() על כל עמודה בו-זמנית. זה שימושי עבור המרות רחבות וסוחפות, כמו עיצוב או המרת סוג, על פני כל התאים.
DataFrame.applymap() מבוטל. הדרך המומלצת החדשה היא להשתמש ב-DataFrame.map(). הפונקציונליות זהה. אנו נשתמש ב-applymap() כאן לצורך תאימות, אך היו מודעים לשינוי זה עבור קוד עתידי.
דוגמה מעשית
נניח שיש לנו תת-DataFrame עם העמודות המספריות שלנו בלבד ואנחנו רוצים לעצב את כולן כמחרוזות מטבע עבור דוח.
numeric_df = df[['Price_USD', 'Quantity', 'Total_Cost']]
# Using a lambda function to format each number
formatted_df = numeric_df.applymap(lambda x: f'${x:,.2f}')
print(formatted_df)
פלט:
Price_USD Quantity Total_Cost
0 $1,200.00 $1.00 $1,200.00
1 $25.00 $2.00 $50.00
2 $75.00 $1.00 $75.00
3 $300.00 $2.00 $600.00
4 $50.00 $1.00 $50.00
5 $150.00 $1.00 $150.00
6 $250.00 $1.00 $250.00
7 $30.00 $3.00 $90.00
שימוש נפוץ נוסף הוא לנקות DataFrame של נתוני מחרוזת על ידי, למשל, המרת הכל לאותיות קטנות.
string_df = df[['Product', 'Category', 'Country']].copy() # Create a copy to avoid SettingWithCopyWarning
# Ensure all values are strings to prevent errors
string_df = string_df.astype(str)
lower_df = string_df.applymap(str.lower)
print(lower_df)
מתי להשתמש ב-applymap(): סיכום מהיר
- כאשר עליך להחיל פונקציה פשוטה אחת על כל אלמנט ב-DataFrame.
- עבור משימות כמו המרת סוג נתונים, עיצוב מחרוזות או המרות מתמטיות פשוטות על פני כל ה-DataFrame.
- זכור את ביטולו לטובת
DataFrame.map()בגירסאות Pandas האחרונות.
צלילה עמוקה בביצועים: וקטוריזציה לעומת איטרציה
הלולאה ה"נסתרת"
זהו המושג הקריטי ביותר לתפיסה עבור כתיבת קוד Pandas בעל ביצועים גבוהים. בעוד ש-apply(), map() ו-applymap() נוחים, הם בעצם רק מעטפות מפוארות סביב לולאת Python. כאשר אתה משתמש ב-df.apply(..., axis=1), Pandas חוזרת על ה-DataFrame שלך שורה אחר שורה, ומעבירה כל אחת מהן לפונקציה שלך. לתהליך זה יש תקורה משמעותית והוא איטי בהרבה מפעולות המותאמות ב-C או Cython.
כוחה של וקטוריזציה
וקטוריזציה היא הנוהג של ביצוע פעולות על מערכים שלמים (או Series) בבת אחת, במקום על אלמנטים בודדים. Pandas והספרייה הבסיסית שלה, NumPy, מתוכננות במיוחד להיות מהירות להפליא בפעולות וקטוריות.
בואו נחזור לחישוב 'Total_Cost' שלנו. השתמשנו ב-apply(), אבל האם יש דרך וקטורית?
# Method 1: Using apply() (Iteration)
df['Total_Cost'] = df.apply(lambda row: row['Price_USD'] * row['Quantity'], axis=1)
# Method 2: Vectorized Operation
df['Total_Cost_Vect'] = df['Price_USD'] * df['Quantity']
# Check if the results are the same
print(df['Total_Cost'].equals(df['Total_Cost_Vect'])) # Output: True
השיטה השנייה היא וקטורית. היא לוקחת את כל הסדרה 'Price_USD' ומכפילה אותה בסדרה 'Quantity' כולה בפעולה בודדת ומותאמת ביותר. אם היית מתזמן שתי שיטות אלה ב-DataFrame גדול (מיליוני שורות), הגישה הווקטורית לא רק תהיה מהירה יותר - היא תהיה מהירה בסדרי גודל. אנחנו מדברים על שניות לעומת דקות, או דקות לעומת שעות.
מתי apply() הוא בלתי נמנע?
אם וקטוריזציה כל כך מהירה יותר, מדוע קיימות השיטות האחרות הללו? כי לפעמים, הלוגיקה שלך מורכבת מכדי להיות וקטורית. apply() הוא הכלי הדרוש והנכון כאשר:
- לוגיקה תנאית מורכבת: הלוגיקה שלך כוללת הצהרות `if/elif/else` מסובכות התלויות במספר עמודות, כמו בדוגמת `assign_shipping_priority` שלנו. למרות שניתן להשיג חלק מזה עם `np.select()`, זה יכול להפוך לקריא.
- פונקציות ספרייה חיצוניות: עליך להחיל פונקציה מספרייה חיצונית על הנתונים שלך. לדוגמה, החלת פונקציה מספרייה גיאוספציאלית כדי לחשב מרחק על סמך עמודות קו רוחב וקו אורך, או פונקציה מספריית עיבוד שפה טבעית (כמו NLTK) כדי לבצע ניתוח סנטימנטים על עמודת טקסט.
- תהליכים איטרטיביים: החישוב לשורה נתונה תלוי בערך המחושב בשורה קודמת (אם כי זה נדיר ולעתים קרובות סימן לכך שיש צורך במבנה נתונים שונה).
שיטת עבודה מומלצת: וקטוריזציה תחילה, apply() שנייה
זה מוביל לכלל הזהב של ביצועי Pandas:
תמיד חפש פתרון וקטורי תחילה. השתמש ב-apply() כגיבוי החזק והגמיש שלך כאשר פתרון וקטורי אינו מעשי או אפשרי.
סיכום ותובנות מרכזיות: בחירת הכלי הנכון
בואו נמזג את הידע שלנו למסגרת קבלת החלטות ברורה. כאשר אתה מתמודד עם משימת המרה מותאמת אישית, שאל את עצמך את השאלות הבאות:
טבלת השוואה
| שיטה | פועלת על | היקף הפעולה | הפונקציה מקבלת | מקרה שימוש עיקרי |
|---|---|---|---|---|
| וקטוריזציה | Series, DataFrame | מערך שלם בבת אחת | לא רלוונטי (הפעולה ישירה) | פעולות אריתמטיות, לוגיות. הביצועים הגבוהים ביותר. |
.map() |
Series בלבד | אלמנט-אחר-אלמנט | אלמנט יחיד | החלפת ערכים ממילון. |
.apply() |
Series, DataFrame | שורה אחר שורה או עמודה אחר עמודה | Series (שורה או עמודה) | לוגיקה מורכבת תוך שימוש במספר עמודות לשורה. |
.applymap() |
DataFrame בלבד | אלמנט-אחר-אלמנט | אלמנט יחיד | עיצוב או המרת כל תא ב-DataFrame. |
תרשים זרימה של החלטות
- האם ניתן לבטא את הפעולה שלי באמצעות אריתמטיקה בסיסית (+, -, *, /) או אופרטורים לוגיים (&, |, ~) על עמודות שלמות?
→ כן? השתמש בגישה וקטורית. זה הכי מהיר. (לדוגמה, `df['col1'] * df['col2']`) - האם אני עובד רק על עמודה אחת, והאם המטרה העיקרית שלי היא להחליף ערכים על סמך מילון?
→ כן? השתמש ב-Series.map(). היא מותאמת עבור זה. - האם עלי להחיל פונקציה על כל אלמנט בודד בכל ה-DataFrame שלי?
→ כן? השתמש ב-DataFrame.applymap()(אוDataFrame.map()בפנדס חדשות יותר). - האם הלוגיקה שלי מורכבת ודורשת ערכים מעמודות מרובות בכל שורה כדי לחשב תוצאה בודדת?
→ כן? השתמש ב-DataFrame.apply(..., axis=1). זה הכלי שלך עבור לוגיקה מורכבת, לפי שורה.
מסקנה
הניווט באפשרויות ליישום פונקציות מותאמות אישית ב-Pandas הוא מעבר חובה לכל מתרגל נתונים. בעוד שהם עשויים להיראות ניתנים להחלפה במבט ראשון, map(), apply() ו-applymap() הם כלים מובחנים, שלכל אחד מהם חוזקות ומקרי שימוש אידיאליים משלו. על ידי הבנת ההבדלים ביניהם, אתה יכול לכתוב קוד שהוא לא רק נכון אלא גם קריא יותר, ניתן לתחזוקה ויעיל משמעותית.
זכור את ההיררכיה: העדף וקטוריזציה עבור מהירות הגלם שלה, השתמש ב-map() עבור החלפת Series היעילה שלה, בחר ב-applymap() עבור המרות ברחבי DataFrame, והשתמש בכוח ובגמישות של apply() עבור לוגיקה מורכבת לפי שורה או לפי עמודה שלא ניתן לבצע בה וקטוריזציה. מצויד בידע זה, אתה מצויד כעת טוב יותר להתמודד עם כל אתגר מניפולציית נתונים שיגיע בדרכך, ולהפוך נתונים גולמיים לתובנות רבות עוצמה במיומנות וביעילות.